Кластеризация статей уголовного кодекса РФ

In [1]:
from lxml import etree
import pandas as pd
import re
import numpy as np
import nltk
from nltk.stem.snowball import SnowballStemmer

from sklearn.feature_extraction.text import TfidfVectorizer

import plotly
import plotly.graph_objs as go

plotly.offline.init_notebook_mode(connected=False)

Читаем XML файл с УК РФ и записываем в dataframe articles все статьи кодекса

In [2]:
with open('RFCriminalCode.xml') as file:
    tree = etree.parse(file)
In [3]:
root = tree.getroot()
In [4]:
articles = pd.DataFrame(columns = ['number', 'section', 'section_name', 'chapter', 'chapter_name', 'name', 'body'])

for part in root.getchildren():
    for section in part.getchildren():
        for chapter in section.getchildren():
            for article in chapter.getchildren():
                articles = articles.append({'number': article.get('number'),
                                            'section': section.get('number'),
                                            'section_name': section.get('name'),
                                            'chapter': chapter.get('number'),
                                            'chapter_name': chapter.get('name'),
                                            'name': article.get('name'),
                                            'body': article.getchildren()[0].text
                                            }, ignore_index=True)
In [5]:
articles = articles.dropna()

Стемминг и токенизация текстов статей

In [6]:
stemmer = SnowballStemmer('russian', ignore_stopwords=True)
In [7]:
def tokenize_and_stem(text):
    tokens = [word for sent in nltk.sent_tokenize(text) for word in nltk.word_tokenize(sent)]
    only_word_tokens = [token for token in tokens if re.search('[а-яА-Я]', token)]
    return [stemmer.stem(token) for token in only_word_tokens]
In [8]:
articles['stemmed_tokens'] = articles.body.map(tokenize_and_stem)
In [9]:
articles.body[0]
Out[9]:
'1. Уголовное законодательство Российской Федерации состоит из настоящего Кодекса. Новые законы, предусматривающие уголовную ответственность, подлежат включению в настоящий Кодекс. 2. Настоящий Кодекс основывается на Конституции Российской Федерации и общепризнанных принципах и нормах международного права.'
In [10]:
articles.head()
Out[10]:
number section section_name chapter chapter_name name body stemmed_tokens
0 1 I УГОЛОВНЫЙ ЗАКОН 1 ЗАДАЧИ И ПРИНЦИПЫ УГОЛОВНОГО КОДЕКСА РОССИЙСКО... Уголовное законодательство Российской Федерации 1. Уголовное законодательство Российской Федер... [уголовн, законодательств, российск, федерац, ...
1 2 I УГОЛОВНЫЙ ЗАКОН 1 ЗАДАЧИ И ПРИНЦИПЫ УГОЛОВНОГО КОДЕКСА РОССИЙСКО... Задачи Уголовного кодекса Российской Федерации 1. Задачами настоящего Кодекса являются: охран... [задач, настоя, кодекс, явля, охра, прав, и, с...
2 3 I УГОЛОВНЫЙ ЗАКОН 1 ЗАДАЧИ И ПРИНЦИПЫ УГОЛОВНОГО КОДЕКСА РОССИЙСКО... Принцип законности 1. Преступность деяния, а также его наказуемос... [преступн, деян, а, такж, его, наказуем, и, ин...
3 4 I УГОЛОВНЫЙ ЗАКОН 1 ЗАДАЧИ И ПРИНЦИПЫ УГОЛОВНОГО КОДЕКСА РОССИЙСКО... Принцип равенства граждан перед законом Лица, совершившие преступления, равны перед за... [лиц, соверш, преступлен, равн, перед, закон, ...
4 5 I УГОЛОВНЫЙ ЗАКОН 1 ЗАДАЧИ И ПРИНЦИПЫ УГОЛОВНОГО КОДЕКСА РОССИЙСКО... Принцип вины 1. Лицо подлежит уголовной ответственности тол... [лиц, подлеж, уголовн, ответствен, только, за,...

TF-IDF and Cosine simularity

Посчитаем tf-idf для наших документов, убирая экстремальные значения частот (0.2 < tfidf < 0.8)

In [75]:
tfidf_vectorizer = TfidfVectorizer(max_df=0.8, #max_features=200,
                                   min_df=0.2, stop_words=nltk.corpus.stopwords.words('russian'),
                                   use_idf=True, tokenizer=tokenize_and_stem, ngram_range=(1,3))

%time tfidf_matrix = tfidf_vectorizer.fit_transform(articles.body)

print(tfidf_matrix.shape)
CPU times: user 7.34 s, sys: 13.2 ms, total: 7.36 s
Wall time: 7.39 s
(469, 213)
In [76]:
terms = tfidf_vectorizer.get_feature_names()
In [77]:
from sklearn.metrics.pairwise import cosine_distances

dist = cosine_distances(tfidf_matrix)

Different clustering

In [78]:
from sklearn.cluster import KMeans
num_clusters = 12 # Количество разделов уголовного кодекса
%time articles['km_cluster'] = KMeans(n_clusters=num_clusters).fit(tfidf_matrix).labels_.tolist()
CPU times: user 2.69 s, sys: 20.3 ms, total: 2.71 s
Wall time: 2.76 s
In [79]:
from sklearn.cluster import DBSCAN
%time articles['db_cluster'] = DBSCAN(metric='precomputed').fit_predict(tfidf_matrix).tolist()
CPU times: user 8.3 ms, sys: 18 µs, total: 8.32 ms
Wall time: 13.1 ms
In [80]:
from sklearn.cluster import AgglomerativeClustering
%time articles['agg_cluster'] = AgglomerativeClustering(n_clusters=12).fit(tfidf_matrix.toarray()).labels_.tolist()
CPU times: user 26.4 ms, sys: 134 µs, total: 26.5 ms
Wall time: 31.7 ms
In [81]:
from sklearn.cluster import SpectralClustering
%time articles['spec_cluster'] = SpectralClustering().fit(tfidf_matrix).labels_.tolist()
CPU times: user 132 ms, sys: 112 ms, total: 243 ms
Wall time: 223 ms

Multidimentional scaling

In [82]:
from sklearn.manifold import MDS

mds = MDS(n_components=2, dissimilarity="precomputed", random_state=42)
%time pos2 = mds.fit_transform(dist)
mds = MDS(n_components=3, dissimilarity='precomputed', random_state=42)
%time pos3 = mds.fit_transform(dist) 
CPU times: user 6.93 s, sys: 5.62 s, total: 12.6 s
Wall time: 7.53 s
CPU times: user 6.72 s, sys: 5.67 s, total: 12.4 s
Wall time: 7.36 s
In [83]:
articles['2d_pos'] = pos2.tolist()
articles['3d_pos'] = pos3.tolist()
In [84]:
def textfunc(row): return 'Кластер: ' + \
    str(row['km_cluster']) + '<br>Номер статьи: ' + \
    row['number'] + '<br>' + row['name']

data = go.Data([
        go.Scatter(x=articles['2d_pos'].map(lambda x: x[0]),
                           y=articles['2d_pos'].map(lambda x: x[1]),
                           mode='markers',
                           marker=go.Marker(
                               size=8, color=articles['km_cluster'].astype(float), colorscale='Jet'),
                           text=articles.apply(textfunc, axis=1),
                           showlegend=False,
                           hoverinfo='text'),
               
               ])

figure = go.Figure(data=data, layout=go.Layout(
    title='УК РФ TF-IDF MDS KMeans(n_clusters=12)'))
plotly.offline.iplot(figure)
In [85]:
def textfunc(row): return 'Кластер: ' + \
    str(row['spec_cluster']) + '<br>Номер статьи: ' + \
    row['number'] + '<br>' + row['name']

data = go.Data([
        go.Scatter(x=articles['2d_pos'].map(lambda x: x[0]),
                           y=articles['2d_pos'].map(lambda x: x[1]),
                           mode='markers',
                           marker=go.Marker(
                               size=8, color=articles['spec_cluster'].astype(float), colorscale='Jet'),
                           text=articles.apply(textfunc, axis=1),
                           showlegend=False,
                           hoverinfo='text'),
               
               ])

figure = go.Figure(data=data, layout=go.Layout(
    title='УК РФ TF-IDF MDS Spectral clustering'))
plotly.offline.iplot(figure)
In [86]:
data = go.Data([go.Scatter3d(x=articles['3d_pos'].map(lambda x: x[0]),
                             y=articles['3d_pos'].map(lambda x: x[1]),
                             z=articles['3d_pos'].map(lambda x: x[2]),
                             mode='markers',
                             marker=go.Marker(
                                 size=3, color=articles['km_cluster'], colorscale='Jet'),
                             text=articles.apply(textfunc, axis=1),
                             hoverinfo='text')])

figure = go.Figure(data=data, layout=go.Layout(
    title='УК РФ TF-IDF MDS KMeans(n_clusters=12)'))
plotly.offline.iplot(figure)
In [87]:
def sectiontextfunc(row): return '<br>Номер статьи: ' + \
    row['number'] + '<br>' + row['name'] + '<br>Раздел ' + row['section'] + ' ' + row['section_name']
    
data = go.Data([
        go.Scatter(x=articles['2d_pos'].map(lambda x: x[0]),
                           y=articles['2d_pos'].map(lambda x: x[1]),
                           mode='markers',
                           marker=go.Marker(
                               size=8, color=articles['section'].astype('category').cat.codes, colorscale='Jet'),
                           text=articles.apply(sectiontextfunc, axis=1),
                           showlegend=False,
                           hoverinfo='text'),
               
               ])

figure = go.Figure(data=data, layout=go.Layout(
    title='УК РФ TF-IDF MDS Разделы кодекса'))
plotly.offline.iplot(figure)
In [88]:
def sectiontextfunc(row): return '<br>Номер статьи: ' + \
    row['number'] + '<br>' + row['name'] + '<br>Раздел ' + row['section'] + ' ' + row['section_name']
    
data = go.Data([go.Scatter3d(x=articles['3d_pos'].map(lambda x: x[0]),
                             y=articles['3d_pos'].map(lambda x: x[1]),
                             z=articles['3d_pos'].map(lambda x: x[2]),
                             mode='markers',
                             marker=go.Marker(
                                 size=3, color=articles['section'].astype('category').cat.codes, colorscale='Jet'),
                             text=articles.apply(sectiontextfunc, axis=1),
                             hoverinfo='text')])

figure = go.Figure(data=data, layout=go.Layout(
    title='УК РФ TF-IDF Разделы кодекса'))
plotly.offline.iplot(figure)

t-SNE

In [95]:
from sklearn.manifold import TSNE
%time articles['2d_tsne'] = TSNE(perplexity=5, metric='precomputed').fit_transform(dist).tolist()
CPU times: user 3.51 s, sys: 536 ms, total: 4.04 s
Wall time: 4.2 s
In [96]:
%time articles['3d_tsne'] = TSNE(n_components=3,perplexity=5, metric='precomputed').fit_transform(dist).tolist()
CPU times: user 15.8 s, sys: 1.17 s, total: 17 s
Wall time: 17.5 s
In [98]:
def textfunc(row): return 'Кластер: ' + \
    str(row['agg_cluster']) + '<br>Номер статьи: ' + \
    row['number'] + '<br>' + row['name']
data = go.Data([go.Scatter(x=articles['2d_tsne'].map(lambda x: x[0]),
                           y=articles['2d_tsne'].map(lambda x: x[1]),
                           mode='markers',
                           marker=go.Marker(
                               size=10, color=articles['agg_cluster'], colorscale='Jet'),
                           text=articles.apply(textfunc, axis=1),
                           hoverinfo='text')])
layout = go.Layout(title='УК РФ TF-IDF t-SNE(perplexity=5) Agglomerative Clustering')
figure = go.Figure(data=data, layout=layout)
plotly.offline.iplot(figure)
In [100]:
def textfunc(row): return 'Кластер: ' + \
    str(row['agg_cluster']) + '<br>Номер статьи: ' + \
    row['number'] + '<br>' + row['name']
data = go.Data([go.Scatter3d(x=articles['3d_tsne'].map(lambda x: x[0]),
                             y=articles['3d_tsne'].map(lambda x: x[1]),
                             z=articles['3d_tsne'].map(lambda x: x[2]),
                             mode='markers',
                             marker=go.Marker(
                                 size=3, color=articles['agg_cluster'], colorscale='Jet'),
                             text=articles.apply(textfunc, axis=1),
                             hoverinfo='text')])

figure = go.Figure(data=data, layout=go.Layout(
    title='УК РФ TF-IDF t-SNE(perplexity=5) Agglomerative clustering'))
plotly.offline.iplot(figure)
In [105]:
data = go.Data([go.Scatter(x=articles['2d_tsne'].map(lambda x: x[0]),
                           y=articles['2d_tsne'].map(lambda x: x[1]),
                           mode='markers',
                           marker=go.Marker(
                               size=10, color=articles['section'].astype('category').cat.codes, colorscale='Jet'),
                           text=articles.apply(sectiontextfunc, axis=1),
                           hoverinfo='text')])
layout = go.Layout(title='УК РФ TF-IDF t-SNE(perplexity=5) Разделы кодекса')
figure = go.Figure(data=data, layout=layout)
plotly.offline.iplot(figure)
In [104]:
data = go.Data([go.Scatter3d(x=articles['3d_tsne'].map(lambda x: x[0]),
                             y=articles['3d_tsne'].map(lambda x: x[1]),
                             z=articles['3d_tsne'].map(lambda x: x[2]),
                             mode='markers',
                             marker=go.Marker(
                                 size=3, color=articles['section'].astype('category').cat.codes, colorscale='Jet'),
                             text=articles.apply(sectiontextfunc, axis=1),
                             hoverinfo='text')])

figure = go.Figure(data=data, layout=go.Layout(
    title='УК РФ TF-IDF t-SNE(perplexity=5) Разделы кодекса'))
plotly.offline.iplot(figure)

Latent Dirichlet Allocation (LDA)

Возможно стоило удалить все имена собственные из текстов статей. Однако, стоит заметить, что кроме российская федерация формулировок с именами собственными natasha не находит:

In [30]:
# from natasha import LocationExtractor
# extractor = LocationExtractor()
# for text in articles['body']:
#     matches = extractor(text)
#     for match in matches:
#         print(match.span, match.fact)
In [31]:
import string
def strip_proppers(text):
    tokens = [word for sent in nltk.sent_tokenize(text) for word in nltk.word_tokenize(sent) if word.islower()]
    return ''.join([" " + i if not i.startswith("'") and i not in string.punctuation else i for i in tokens]).strip()
In [32]:
from nltk.tag import pos_tag

def strip_proppers_POS(text):
    tagged = pos_tag(text.split()) #use NLTK's part of speech tagger
    non_propernouns = [word for word,pos in tagged if pos != 'NNP' and pos != 'NNPS']
    return non_propernouns
In [33]:
from gensim import corpora, models, similarities 

#remove proper names
%time preprocess = [strip_proppers(doc) for doc in articles['body']]

#tokenize
%time tokenized_text = [tokenize_and_stem(text) for text in preprocess]

#remove stop words
%time texts = [[word for word in text if word not in nltk.corpus.stopwords.words('russian')] for text in tokenized_text]
CPU times: user 1.63 s, sys: 90 ms, total: 1.72 s
Wall time: 1.81 s
CPU times: user 7.63 s, sys: 230 ms, total: 7.86 s
Wall time: 8.23 s
CPU times: user 17.1 s, sys: 1.93 s, total: 19 s
Wall time: 20.5 s
In [34]:
dictionary = corpora.Dictionary(texts)
dictionary.filter_extremes(no_below=1, no_above=0.8)

corpus = [dictionary.doc2bow(text) for text in texts]
In [35]:
%time lda = models.LdaModel(corpus, num_topics=5, id2word=dictionary, update_every=5, chunksize=10000, passes=100)
CPU times: user 4min 22s, sys: 3.79 s, total: 4min 26s
Wall time: 4min 34s
In [36]:
lda.show_topics(formatted=True, num_words=5)
Out[36]:
[(0,
  '0.023*"лиц" + 0.021*"размер" + 0.016*"преступлен" + 0.013*"ин" + 0.010*"совершен"'),
 (1,
  '0.037*"наказан" + 0.036*"преступлен" + 0.023*"част" + 0.023*"стат" + 0.020*"осужден"'),
 (2,
  '0.057*"лет" + 0.044*"размер" + 0.030*"занима" + 0.030*"определен" + 0.024*"работ"'),
 (3,
  '0.023*"лиц" + 0.020*"преступлен" + 0.016*"медицинск" + 0.015*"совершен" + 0.015*"опасн"'),
 (4,
  '0.043*"лет" + 0.041*"свобод" + 0.027*"наказыва" + 0.025*"стат" + 0.022*"част"')]
In [ ]:
 
In [ ]:
 
In [ ]: